﻿
using CommunityToolkit.Mvvm.ComponentModel;
using HslCommunication;
using HslCommunication.ModBus;
using HslCommunication.Profinet.Siemens;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MiniExcelLibs;
using ProductionLine.Modules.ShareLib.Models;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace ProductionLine.Modules.ShareLib.Helpers;
/// <summary>
/// IDisposable:因为有一些资源需要释放
/// </summary>
public partial class GlobalConfig : ObservableObject, IDisposable {
    #region 属性/字段
    [ObservableProperty]
    private ModbusParams _modbusParams = new();

    public ModbusTcpNet _modbusTcpNet = new();


    //日志记录
    private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger();

    /// <summary>
    /// plc连接是否成功
    /// </summary>
    [ObservableProperty]
    private bool _plcConnected;

    /// <summary>
    /// modbus tcp plc连接是否成功
    /// </summary>
    [ObservableProperty]
    private bool _plcModbusTcpConnected;

    /// <summary>
    /// 西门子s7
    /// </summary>
    public SiemensS7Net Plc { get; set; }
    /// <summary>
    /// modbus rtu plc
    /// </summary>
    public ModbusRtu PlcRtu { get; set; }

    /// <summary>
    /// 并发字典，所有的取数都从这个字典里面进行获取; key:地址,value:值
    /// </summary>
    public ConcurrentDictionary<string, object> ReadDataDic { get; set; } = new();

    /// <summary>
    /// 读/写实体类:配置包里面的excel文件
    /// </summary>
    public List<ReadEntity> ReadEntities { get; set; } = new();//excel文件表结构-read,初始化时InitExcelsAddress赋值了,从excel读取的
    public List<WriteEntity> WriteEntities { get; set; } = new();//excel文件表结构-write

    /// <summary>
    /// 只读plc属性
    /// </summary>
    private readonly PlcParam _plcParam;

    /// <summary>
    /// 取消令牌
    /// </summary>
    private CancellationTokenSource _cts = new();
    private CancellationTokenSource _ctsSave = new();

    /// <summary>
    /// 并发队列
    /// </summary>
    private ConcurrentQueue<ScadaReadData> _scadaReadDataQueue = new();

    /// <summary>
    /// 可观察属性,用于绑定到xaml页面
    /// </summary>
    [ObservableProperty]
    private ScadaReadData _scadaReadDataProp = new();

    /// <summary>
    /// 用户会话
    /// </summary>
    private readonly UserSession _userSession;


    private readonly ModbusTcpConfig modbusTcpConfig;
    #endregion

    /// <summary>
    /// 构造函数,初始化
    /// </summary>
    /// <param name="options"></param>
    /// <param name="logger"></param>
    public GlobalConfig(UserSession userSession, PlcParam plcParam, ModbusTcpConfig _modbusTcpConfig) {
        modbusTcpConfig = _modbusTcpConfig;
        _plcParam = plcParam;
        _userSession = userSession;
        InitPlc();//plc初始化,不含plc连接操作
     
        InitPlcTcp();//modbus tcp初始化

        InitExcelsAddress();//Excel数据→List<Entity>
        //InitDequeue();

        //如果json中开启了自动采集,则启动采集
        if (plcParam.AutoCollect) {//开启了自动采集
            StartCollectionAsync();
        } else {//如果没开启,则自己读一遍
            UpdateControlData();
        }

        //如果json中开启了自动模拟,则启动模拟
        if (plcParam.AutoMock) {
            StartMockData();
        }
    }

    private async Task InitPlcTcp() {
        _modbusTcpNet.IpAddress = modbusTcpConfig.IpAddress;
        _modbusTcpNet.Port = modbusTcpConfig.Port;
        _modbusTcpNet.ConnectTimeOut = modbusTcpConfig.ConnectTimeOut;
        _modbusTcpNet.ReceiveTimeOut = modbusTcpConfig.ReceiveTimeOut;
        _modbusTcpNet.Station = 1;//服务端地址码
        _modbusTcpNet.AddressStartWithZero = true;
        _modbusTcpNet.IsCheckMessageId = true;//检查id
        _modbusTcpNet.IsStringReverse = false;//字符反转
        _modbusTcpNet.DataFormat = HslCommunication.Core.DataFormat.CDAB;
        await InitPlcTcpServer();


    }
    /// <summary>
    /// 连接modbus tcp服务端
    /// </summary>
    /// <returns></returns>
    public  async Task InitPlcTcpServer() {
        var connectResult = await _modbusTcpNet.ConnectServerAsync();
        if (!connectResult.IsSuccess) {
            Logger.Error($"ModbusTcp 连接失败:{_modbusTcpNet.IpAddress}:{_modbusTcpNet.Port}");
        } else {
            Logger.Info($"ModbusTcp 连接成功:{_modbusTcpNet.IpAddress}:{_modbusTcpNet.Port}");
            await Task.Run(async () => {
                while (true) {
                    var contentResult = await _modbusTcpNet.ReadInt16Async("100", 8);
                    if (contentResult.IsSuccess) {

                        var content = contentResult.Content;

                        _modbusParams.PlantTemperature = content[0];
                        _modbusParams.PlanHumidity = content[1];
                        _modbusParams.PlanProduction = content[2];
                        _modbusParams.ActualYield = content[3];
                        _modbusParams.BadCounting = content[4];
                        _modbusParams.ProductionBeats = content[5];
                        _modbusParams.AccumulateAlarms = content[6];
                        _modbusParams.CumulativeProduction = content[7];

                    }
                    await Task.Delay(3000);
                }
            });
          
          
            }




        }

  

    #region 自动采集
        /// <summary>
        /// 启动plc,并开启自动采集
        /// </summary>
    public async Task StartCollectionIndex() {
        if (!PlcConnected) { // 如果plc未连接,则尝试连接
            await InitPlcServer();
        }
        if (_plcParam.AutoCollect && PlcConnected) { // 根据配置文件AutoCollect决定是否自动启动采集,默认自动采集启动
            await StartCollectionAsync();
        }
    }

    #endregion

    #region 数据虚拟

    private CancellationTokenSource _ctsMock = new();

    public void StopMockData() {
        if (_ctsMock != null) {
            _ctsMock.Cancel();
            _ctsMock.Dispose();
        }
    }
    /// <summary>
    /// 虚拟数据
    /// </summary>
    public void StartMockData() {
        _ctsMock = new CancellationTokenSource();
        Task.Run(async () => {
            var scadaData = new ScadaReadData();
            // 获取所有浮点型的属性
            var properties = typeof(ScadaReadData).GetProperties()
            .Where(p => p.PropertyType == typeof(float));
            // 获取 Bool 值
            var propertiesBool = typeof(ScadaReadData).GetProperties()
                .Where(p => p.PropertyType == typeof(bool));

            while (!_ctsMock.Token.IsCancellationRequested) {
                var random = new Random();

                foreach (var property in properties) {
                    var value = (float)(random.NextDouble() * 4 + 1);

                    var address = ReadEntities.FirstOrDefault(
                        x => x.En == property.Name)?.Address;

                    if (!string.IsNullOrEmpty(address)) {
                        await Plc.WriteAsync(address, value);
                    }
                }

                foreach (var property in propertiesBool) {
                    var x = ReadEntities.FirstOrDefault(
                        x => x.En == property.Name);
                    if (x.Module == "Control") continue;
                    var address = x?.Address;

                    //去除那些开关

                    if (!string.IsNullOrEmpty(address)) {
                        if (random.Next(0, 2) != 0) {
                            await Plc.WriteAsync(address, true);
                        } else {
                            await Plc.WriteAsync(address, false);
                        }
                    }
                }

                await Task.Delay(1000);
            }
        }, _ctsMock.Token);
    }

    #endregion

    #region 初始化方法
    /// <summary>
    /// todo 上升沿触发，写入数据库
    /// </summary>

    private void InitDequeue() {//出队方法
        Task.Run(async () => {
            while (!_ctsSave.Token.IsCancellationRequested) {
                try {
                    // 从队列中取出数据
                    if (_scadaReadDataQueue.TryDequeue(out var data)) {
                        // 往数据库里面写
                        var result = await SqlSugarHelper.Db.Insertable(data).ExecuteCommandAsync();//返回的是更新条数

                    } else {
                        await Task.Delay(100, _ctsSave.Token);
                    }

                } catch (Exception e) {
                    Debug.WriteLine(e);
                }
            }
        }, _ctsSave.Token);
    }

    /// <summary>
    /// 将配置中的两个excel文件植入到上面两个entity数组中
    /// Excel数据→List<Entity>
    /// </summary>
    public void InitExcelsAddress() {
        var rootPath = AppDomain.CurrentDomain.BaseDirectory + "Configs\\";
        var readPath = rootPath + "TulingRead.xlsx";
        var writePath = rootPath + "TulingWrite.xlsx";

        if (!File.Exists(readPath) || !File.Exists(writePath)) { //读写路径存在
            Logger.Error($"找不到读/写配置文件 xlsx{readPath}\n{writePath}");//记录日志
            return;
        }
        try {
            ReadEntities = MiniExcel.Query<ReadEntity>(readPath)
                .Where(x => !string.IsNullOrEmpty(x.Address))
                .ToList();
            WriteEntities = MiniExcel.Query<WriteEntity>(writePath)
                .Where(x => !string.IsNullOrEmpty(x.Address))
                .ToList();
        } catch (Exception e) {
            Logger.Error($"MiniExcel 读取文件异常{e.Message}");
        }
    }

    /// <summary>
    /// plc初始化,初始化信息还是从配置里面读取
    /// </summary>
    public void InitPlc() {
        Plc = new SiemensS7Net(SiemensPLCS.S1200, _plcParam.PlcIp);//实例化一个西门子的S7协议的通讯对象并指定Ip地址
        Plc.Port = _plcParam.PlcPort;//端口
        Plc.Slot = _plcParam.PlcSlot;
        Plc.Rack = _plcParam.PlcRack;
        Plc.ConnectTimeOut = _plcParam.PlcConnectTimeOut;//超时时间
        //hsl有断线重连的功能,所以不需要配置重连时间
    }

    /// <summary>
    /// 做PLC连接操作
    /// </summary>
    public async Task InitPlcServer() {
        try {
            var opResult = await Plc.ConnectServerAsync();//plc连接
            // 给PlcConnected赋值为plc状态
            PlcConnected = opResult.IsSuccess;

            if (!opResult.IsSuccess) {//plc连接失败
                Logger.Error($"PLC 连接失败:{_plcParam.PlcIp}:{_plcParam.PlcPort}");
            }
        } catch (Exception e) {
            PlcConnected = false;//出异常也要赋值
            Logger.Error($"PLC 连接异常：{e.Message}");
        }
    }
    #endregion

    /// <summary>
    /// 开始采集，写入读取字典 ReadDataDic
    /// </summary>
    /// <param name="externalToken"></param>
    /// <returns></returns>
    public async Task StartCollectionAsync(CancellationToken? externalToken = null) {
        StopCollection();//如果之前有任务在运行，先停止
        //if (!PlcConnected) MessageBox.Show("请开启plc");
        // 令牌创建,返回一个空 CancellationToken 值。
        _cts = CancellationTokenSource.CreateLinkedTokenSource(externalToken ?? CancellationToken.None);
        // 读取数据任务
        await Task.Run(async () => {
            //设置plc开启才开始读取数据
            while (!_cts.Token.IsCancellationRequested) {

                //等待plc连接
                while (!PlcConnected || !_plcParam.AutoCollect) {
                    if (!PlcConnected && !_plcParam.AutoCollect) {
                        if (_cts.Token.IsCancellationRequested) break;
                        _cts.Cancel();
                    }

                    continue;
                }
                //读取数据
                try {
                    // 批量读取下位机数据赋值给字典
                    await UpdateControlData();
                    await UpdateMonitorData();
                    await UpdateProcessData();
                    //这里 : 数据全都存在字典里,但并不是都存到数据库里面
                    //判定是否保存到数据库里面,看excel最后一个字段save=true,
                    //我们需要一次性将其存到数据库里面,但是,这是不合理的,一直读写,消耗资源
                    //我们需要定义一个上升沿,上升沿触发之后→上下位机同步数据保存,由下位机进行数据保存的控制
                    await SaveAsync(); //不是直接往数据库里面写，
                                       //而是先写入到队列中，然后再（批量）写入到数据库
                                       //这样采集和写入分离开,互不影响

                    await Task.Delay(_plcParam.PlcCycleInterval, _cts.Token);//读取间隔,token

                } catch (OperationCanceledException) {//因为取消任务也会有异常,所以...
                                                      // 正常取消，退出循环
                    break;
                } catch (Exception e) {
                    Logger.Error(e.Message);
                }
            }
        }, _cts.Token);
        return;
    }

    /// <summary>
    /// 不是直接将字典数据往数据库里面写，
    /// 而是将字典数据先写入到队列中，然后再（批量）写入到数据库
    /// </summary>
    /// <returns></returns>
    private async Task SaveAsync() {
        //需要存储到数据库的数据项
        var saveAddress = ReadEntities.Where(x => x.Save).ToList();//哪些字段是true(也就是需要存储)

        if (!saveAddress.Any()) return;//

        // 创建新的数据对象(算是字典中所有需要存放到数据库的数据的Key值)
        var scadaReadData = new ScadaReadData() {
            CreateTime = DateTime.Now,
            UpdateTime = DateTime.Now,
        };
        //需要存储到数据库的数据项的每一项
        foreach (var address in saveAddress) {//如果每次都创建新对象，会导致性能问题

            var property = typeof(ScadaReadData).GetProperty(address.En);//获取key值

            if (property != null && ReadDataDic.TryGetValue(address.En, out var value)) {
                property.SetValue(scadaReadData, value);//将value按照key值存到scadaReadData里面
            }
        }
        // 将数据添加到队列中
        _scadaReadDataQueue.Enqueue(scadaReadData);
        // 批量写入数据库....
        InitDequeue();//启动出队方法
    }

    #region 读取数据
    private async Task UpdateProcessData() {
        await UpdatePlcToReadDataDic<float>("Data", "DBD");
    }

    private async Task UpdateMonitorData() {
        await UpdatePlcToReadDataDic<bool>("Monitor", "DBX");
    }

    private async Task UpdateControlData() {
        await UpdatePlcToReadDataDic<bool>("Control", "DBX");
    }
    #endregion


    /// <summary>
    /// 读取下位机数据到ReadDataDic
    /// </summary>
    /// <typeparam name="T">支持的类型</typeparam>
    /// <param name="module">模块</param>
    /// <param name="addressType">地址类型</param>
    /// <returns></returns>
    private async Task UpdatePlcToReadDataDic<T>(string module, string addressType) {
        try {
            // 筛选出指定模块和地址类型的数据项列表
            var addresses = ReadEntities.Where(x => x.Module == module && x.Address.Contains(addressType)).ToList();

            if (addresses.Count < 1) {
                return;
            }

            OperateResult<T[]> result;
            // 真正开始从下位机读取数据
            if (typeof(T) == typeof(bool)) {


                if (module == "Monitor") {

                    #region
                    // 判断各工位是否开启（可优化为批量读取）
                    bool isDegreas = (await Plc.ReadBoolAsync("DB1.DBX1.5")).Content;   // 脱脂工位开
                    bool isRoughWash = (await Plc.ReadBoolAsync("DB1.DBX1.7")).Content; // 粗洗工位开
                    bool isPhosphat = (await Plc.ReadBoolAsync("DB1.DBX2.1")).Content;  // 陶化工位开
                    bool isFineWash = (await Plc.ReadBoolAsync("DB1.DBX2.3")).Content;  // 精洗工位开

                    bool isMoisture = (await Plc.ReadBoolAsync("DB1.DBX2.5")).Content;   // 水分炉工位开
                    bool isCooling = (await Plc.ReadBoolAsync("DB1.DBX2.7")).Content; // 冷却室工位开
                    bool isCuring = (await Plc.ReadBoolAsync("DB1.DBX3.1")).Content;      // 固化炉工位开
                    bool isConvey = (await Plc.ReadBoolAsync("DB1.DBX3.3")).Content;  // 输送机工位开
                    // 过滤：只有满足条件的才保留
                    addresses = addresses.Where(addr => {
                        string en = addr.En;

                        // 判断是否是报警项
                        bool isAlarm = en.Contains("Alarm");

                        // 如果是报警，始终保留
                        if (isAlarm) {
                            return true;
                        }

                        // 如果不是报警，则根据工位状态决定
                        if (en.Contains("Degreas") && !isDegreas) return false;
                        if (en.Contains("RoughWash") && !isRoughWash) return false;
                        if (en.Contains("Phosphat") && !isPhosphat) return false;
                        if (en.Contains("FineWash") && !isFineWash) return false;
                        if (en.Contains("Moisture") && !isMoisture) return false;
                        if (en.Contains("Cooling") && !isCooling) return false;
                        if (en.Contains("Curing") && !isCuring) return false;
                        if (en.Contains("Convey") && !isConvey) return false;

                        return true; // 默认保留
                    }).ToList();

                    #endregion

                    #region MyRegion
                    //if (!((await Plc.ReadBoolAsync("DB1.DBX1.5")).Content)) {//脱脂工位关闭
                    //    addresses = addresses.Where(x => !(x.En.Contains("Degreas")) && (x.En.Contains("Alarm"))).ToList();
                    //}
                    //if (!((await Plc.ReadBoolAsync("DB1.DBX1.7")).Content)) {//粗洗工位关闭
                    //    addresses = addresses.Where(x => !(x.En.Contains("RoughWash")) && (x.En.Contains("Alarm"))).ToList();
                    //}

                    //if (!((await Plc.ReadBoolAsync("DB1.DBX2.1")).Content)) {//陶化工位关闭
                    //    addresses = addresses.Where(x => !(x.En.Contains("Phosphat")) && (x.En.Contains("Alarm"))).ToList();
                    //}
                    //if (!((await Plc.ReadBoolAsync("DB1.DBX2.3")).Content)) {//精洗工位关闭
                    //    addresses = addresses.Where(x => !(x.En.Contains("FineWash")) && (x.En.Contains("Alarm"))).ToList();
                    //}
                    //if (!((await Plc.ReadBoolAsync("DB1.DBX2.5")).Content)) {//水分炉工位关闭
                    //    addresses = addresses.Where(x => !(x.En.Contains("Moisture")) && (x.En.Contains("Alarm"))).ToList();
                    //}
                    //if (!((await Plc.ReadBoolAsync("DB1.DBX2.7")).Content)) {//冷却室工位关闭
                    //    addresses = addresses.Where(x => !(x.En.Contains("Cooling")) && (x.En.Contains("Alarm"))).ToList();
                    //}
                    //if (!((await Plc.ReadBoolAsync("DB1.DBX3.1")).Content)) {//固化炉工位关闭
                    //    addresses = addresses.Where(x => !(x.En.Contains("Curing")) && (x.En.Contains("Alarm"))).ToList();
                    //}
                    //if (!((await Plc.ReadBoolAsync("DB1.DBX3.3")).Content)) {//输送机工位关闭
                    //    addresses = addresses.Where(x => !(x.En.Contains("Convey")) && (x.En.Contains("Alarm"))).ToList();
                    //}
                    #endregion


                    result = await Plc.ReadBoolAsync(addresses.First().Address, (ushort)addresses.Count) as OperateResult<T[]>;
                } else {
                    result = await Plc.ReadBoolAsync(addresses.First().Address, (ushort)addresses.Count) as OperateResult<T[]>;
                }

                // result = await Plc.ReadBoolAsync(addresses.First().Address, (ushort)addresses.Count) as OperateResult<T[]>;

            } else if (typeof(T) == typeof(float)) {
                result = await Plc.ReadFloatAsync(addresses.First().Address, (ushort)addresses.Count) as OperateResult<T[]>;
            } else {
                Logger.Error("不支持传入的类型");
                return;
            }

            // 将 result 结果放入到字典中
            if (!result.IsSuccess) {
                Logger.Error("数据读取失败");

                return;
            }
            //读取下位机成功,更新字典(数据项名称,具体数据)
            for (int i = 0; i < addresses.Count; i++) {
                if (ReadDataDic.ContainsKey(addresses[i].En)) {
                    ReadDataDic[addresses[i].En] = result.Content[i];
                } else {
                    ReadDataDic.TryAdd(addresses[i].En, result.Content[i]);
                }
            }
        } catch (Exception e) {
            Logger.Error(e.Message);
        }
    }

    /// <summary>
    /// 取消令牌,释放资源
    /// </summary>
    public void StopCollection() {
        try {
            if (_cts != null) {
                _cts.Cancel();
                _cts.Dispose();
            }
        } catch (Exception e) {
            Logger.Error(e.Message);
        }
    }

    public void StopSave() {
        try {
            if (_ctsSave != null) {
                _ctsSave.Cancel();
                _ctsSave.Dispose();
            }
        } catch (Exception e) {
            Logger.Error(e.Message);
        }
    }

    /// <summary>
    /// 获取实时字典数值
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="key"></param>
    /// <returns></returns>
    public T GetValue<T>(string key) {
        if (ReadDataDic.TryGetValue(key, out object value)) {
            return (T)value;
        }

        return default;
    }

    #region IDisposable接口方法
    public void Dispose() { }
    #endregion

}